﻿/////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2009 Eric Bowen
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 
// associated documentation files (the "Software"), to deal in the Software without restriction, including 
// without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
// copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the 
// following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial 
// portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 
// LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
/////////////////////////////////////////////////////////////////////////////////////////////////////
// Author's blog: http://blog.scrappydog.com
// Updated source code available:  http://scrappydb.codeplex.com
/////////////////////////////////////////////////////////////////////////////////////////////////////

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Web.Caching;
using Amazon.SimpleDB.Model;
using ScrappyDB.Attributes;
using ScrappyDB.BaseClasses;
using ScrappyDB.Utilities;

namespace ScrappyDB
{
    public partial class Db
    {
        #region IDb Members

        public void SaveChanges<T>(T o) where T : class, new()
        {
            Log.Info("DB.SaveChanges Start: " + DateTime.Now.ToLongTimeString());

            //o.db = this;

            BatchPutAttributesRequest batchRequest = CreateBatchRequest(o);

            ReplaceableItem batchItem = CreateBatchRequestItem(o);

            batchRequest.WithItem(batchItem);

            //TODO: EAB - convert this to a NON-BATCH save
            BatchPut(batchRequest, r =>
                            {
                                var response = this.Service.EndBatchPutAttributes(r);
                                if (response.IsSetResponseMetadata())
                                    Utility.TraceResponse(response.ResponseMetadata, response.ToXML());
                            });
        }

        #endregion

        public void BatchPut(BatchPutAttributesRequest batchRequest)
        {
            BatchPut(batchRequest,new AsyncCallback(r =>
            {
                Log.Info("BatchPut: Async Callback");
                var response = Service.EndBatchPutAttributes(r);
                if (response.IsSetResponseMetadata())
                    Utility.TraceResponse(response.ResponseMetadata, response.ToXML());
            }));
            return;
            
        }

        private void BatchPut(BatchPutAttributesRequest batchRequest, AsyncCallback callback)
        {
            Log.Info("ScrappyDB.BatchPut: {0} items ", batchRequest.Item.Count);

            try
            {
                var result = Service.BeginBatchPutAttributes(batchRequest, callback, null);
                //TODO: EAB - deal with result value
            }
            catch (Exception ex)
            {
                if (ex.Message.Contains("does not have permission"))
                    throw new SdbExceptions.SdbSecurityException(ex.Message);
                throw;
            }
        }

        public BatchPutAttributesRequest CreateBatchRequest<T>(T entity) where T : class, new()
        {
            string domainName = EntityHelper.GetDomainName(entity);

            var domain = new Domains(WebCache);
            domain.EnsureDomain(domainName);

            return CreateBatchRequest(domainName);
        }

        public ReplaceableItem CreateBatchRequestItem<T>(T entity) where T : class, new()
        {
            //string itemNameValue = EntityHelper.GetEncodedItemNameValue(entity);
            string encodedItemNameValue = GetPropValueFromSimpleTypes(EntityHelper.GetItemNameProperty(entity), entity);

            var batchItem = new ReplaceableItem { ItemName = encodedItemNameValue };

            AddEntityNameAttribute(entity, batchItem);

            foreach (PropertyInfo prop in entity.GetType().GetProperties())
            {
                //skip properties flagged as do not persist
                if (DoNotPersist(prop))
                    continue;

                //skip the create/update date fields if the [AutoCreateUpdateDate] is not set
                if (NotAutoCreateUpdateDates(entity, prop))
                    continue;

                //set LastUpdated to Now
                SetLastUpdated(entity, prop);

                //skip CreateDate if it hasn't been set to a value
                if (CreateDateNotSet(entity, prop))
                    continue;

                //if we got this far this must be a "real" user property that needs to be saved
                if (prop.GetValue(entity, null) != null)
                {
                    CreateAttributesFromProperty(entity, batchItem, prop);
                }
            }

            Log.Debug("ScrappyDB.CreateBatchRequestItem: Entity: {0} - Id: {1} ",
                            EntityHelper.GetEntityName(entity), encodedItemNameValue);

            //clear cache
            ClearEntityFromCache(entity);

            return batchItem;
        }

        private void CreateAttributesFromProperty<T>(T entity, ReplaceableItem batchItem, PropertyInfo prop) where T : class, new()
        {
            //test for valid UTF-8 encoding (SimpleDB data requirement
            //UTF8Encoding utf8 = new UTF8Encoding();
            //Byte[] encodedBytes = utf8.GetBytes(prop.GetValue(entity, null).ToString());
            //var test = encodedBytes.ToString(); 


            Type t = prop.PropertyType;
            if (t.IsGenericType)
            {
                if (t.Name == "List`1")
                {
                    CreateAttributesFromListofString(entity, batchItem, prop);
                }
                else
                {
                    //TODO: need to make sure this is a ScrappyDbCollection

                    //call the save method for this ScrappyDbCollection
                    SaveNestedScrappyDbCollection(entity, prop);
                }
            }
            else
            {
                if (!prop.Name.Equals(EntityHelper.GetItemNamePropertyName(entity)) && prop.Name != "Dirty" &&
                    prop.Name != "Domain")
                {
                    if (prop.PropertyType.BaseType != null && prop.PropertyType.BaseType.FullName == "System.Array")
                    {
                        CreateAttributesFromArray(entity, batchItem, prop);
                    }

                    else
                    {
                        //Log.Info(prop.Name + "=" + prop.GetValue(entity, null));
                        CreateAttributesFromSimpleTypes(entity, batchItem, prop);
                    }
                }
            }
        }

        private static void AddEntityNameAttribute<T>(T entity, ReplaceableItem batchItem)
        {
            if (!(entity.GetType().GetCustomAttributes(typeof(NoEntityNameFieldAttribute), true).Length > 0))
            {
                string entityName = EntityHelper.GetEntityName(entity);
                var entityType = new ReplaceableAttribute
                {
                    Name = "EntityName",
                    Value = entityName,
                    Replace = true
                };
                batchItem.Attribute.Add(entityType);
            }
        }

        private static bool DoNotPersist(PropertyInfo prop)
        {
            return prop.GetCustomAttributes(typeof(DoNotPersistAttribute), true).Length > 0;
        }

        private static bool NotAutoCreateUpdateDates<T>(T entity, PropertyInfo prop)
        {
            return (prop.Name == "CreateDate" | prop.Name == "LastUpdated") &&
                   entity.GetType().GetCustomAttributes(typeof(AutoCreateUpdateDatesAttribute), true).Length == 0;
        }

        private static void SetLastUpdated<T>(T entity, PropertyInfo prop)
        {
            if (prop.Name == "LastUpdated")
                prop.SetValue(entity, DateTime.Now.ToUniversalTime(), null);
        }

        private static bool CreateDateNotSet<T>(T entity, PropertyInfo prop)
        {
            return prop.Name == "CreateDate" && (DateTime)prop.GetValue(entity, null) == DateTime.MinValue;
        }

        private static void CreateAttributesFromListofString<T>(T entity, ReplaceableItem batchItem, PropertyInfo prop)
        {
            if (((List<string>)prop.GetValue(entity, null)).Count > 0)
            {
                //need to treat this same way we treat arrays
                //we are going to assume this is a List<string> for now...
                foreach (string propInfo in (List<string>)prop.GetValue(entity, null))
                {
                    //Log.Info(prop.Name + "=" + propInfo.ToString());

                    var attribute = new ReplaceableAttribute
                    {
                        Name = prop.Name,
                        Value = propInfo,
                        Replace = true
                    };
                    batchItem.Attribute.Add(attribute);
                }
            }
            else
            {
                //we need to clear out all existing values
                //ideally we would do a deleteattributes call here, but it seems to expensive...
                //HACK: so we are just going to save an empty string
                var attribute = new ReplaceableAttribute
                {
                    Name = prop.Name,
                    Value = string.Empty,
                    Replace = true
                };
                batchItem.Attribute.Add(attribute);
            }
        }

        private void SaveNestedScrappyDbCollection<T>(T entity, PropertyInfo prop) where T : class, new()
        {
            //save all members of the collection
            Type t = prop.PropertyType;
            MethodInfo m = t.GetMethod("SaveChanges", new[] { typeof(Cache) });
            m.Invoke(prop.GetValue(entity, null), new object[] { WebCache });

            //SaveChanges the primary key for each member of the collection to the relationship collection
            var nestedCollection = (ICollection)prop.GetValue(entity, null);

            string itemName = EntityHelper.GetEncodedItemNameValue(entity);

            foreach (object nestedEntity in nestedCollection)
            {
                string childEntityName = EntityHelper.GetEntityName(nestedEntity);
                string childItemNameValue = EntityHelper.GetEncodedItemNameValue(nestedEntity);
                string parentEntityName = EntityHelper.GetEntityName(entity);


                var r = new Relationship
                {
                    ParentEntityType = parentEntityName,
                    ParentId = itemName,
                    ChildEntityType = childEntityName,
                    ChildId = childItemNameValue
                };

                SaveChanges(r);
            }
        }

        private static void CreateAttributesFromArray<T>(T entity, ReplaceableItem batchItem, PropertyInfo prop)
        {
            foreach (object propInfo in (Array)prop.GetValue(entity, null))
            {
                //Log.Info(prop.Name + "=" + propInfo.ToString());

                var attribute = new ReplaceableAttribute
                {
                    Name = prop.Name,
                    Value = propInfo.ToString(),
                    Replace = true
                };

                batchItem.Attribute.Add(attribute);
            }
        }

        private static void CreateAttributesFromSimpleTypes<T>(T entity, ReplaceableItem batchItem, PropertyInfo prop)
        {
            if (prop.GetValue(entity, null) != null)
            {
                var propValue = GetPropValueFromSimpleTypes(prop, entity);

                var attribute = new ReplaceableAttribute
                {
                    Name = prop.Name,
                    Value = propValue,
                    Replace = true
                };

                batchItem.Attribute.Add(attribute);
            }
        }

        private static string GetPropValueFromSimpleTypes<T>(PropertyInfo prop, T entity)
        {
            //save the number is an human readable format
            if (prop.GetCustomAttributes(typeof(RawNumberAttribute), true).Length > 0)
                return prop.GetValue(entity, null).ToString();

            //DateTime values
            if (prop.PropertyType == typeof(DateTime))
                return DbDateTime.ConvertDateTimeToIso8601((DateTime)prop.GetValue(entity, null));

            //TODO: add padding and offset for int and long

            if (prop.PropertyType == typeof(ushort))
                return Utility.EncodeNumeric(((ushort)prop.GetValue(entity, null)));

            if (prop.PropertyType == typeof(uint))
                return Utility.EncodeNumeric(((uint)prop.GetValue(entity, null)));

            if (prop.PropertyType == typeof(ulong))
                return Utility.EncodeNumeric(((ulong)prop.GetValue(entity, null)));

            if (prop.PropertyType == typeof(short))
                return Utility.EncodeNumeric(((short)prop.GetValue(entity, null)));

            if (prop.PropertyType == typeof(int))
                return Utility.EncodeNumeric(((int)prop.GetValue(entity, null)));

            if (prop.PropertyType == typeof(long))
                return Utility.EncodeNumeric(((long)prop.GetValue(entity, null)));

            //Double values
            if (prop.PropertyType == typeof(double))
                return Utility.EncodeNumeric((double)prop.GetValue(entity, null));

            //Boolean values
            if (prop.PropertyType == typeof(bool))
                return GetPropValueFromBool(prop, entity);

            //default to string if we don't have a specific type converter
            return prop.GetValue(entity, null).ToString();
        }

        private static string GetPropValueFromBool<T>(PropertyInfo prop, T entity)
        {
            string propValue;

            if (prop.GetCustomAttributes(typeof(OneZeroBoolAttribute), true).Length > 0)
            {
                //store booleans as ones and zeros


                if ((bool)prop.GetValue(entity, null))
                {
                    propValue = "1";
                }
                else
                {
                    propValue = "0";
                }

            }
            else
            {
                //store booleans as "True" or "False"

                propValue = prop.GetValue(entity, null).ToString();
            }

            return propValue;
        }

        private void ClearEntityFromCache<T>(T entity) where T : class, new()
        {
            if (WebCache != null)
            {
                string domainName = EntityHelper.GetDomainName(entity);
                string itemName2 = EntityHelper.GetEncodedItemNameValue(entity);

                WebCache.Remove(domainName + "-" + itemName2);
                Log.Info("CACHE DELETE: key - {0}", EntityCacheKey(domainName, itemName2));
            }
        }

        private static BatchPutAttributesRequest CreateBatchRequest(string domainName)
        {
            Log.Debug("ScrappyDB.CreateBatchRequest: Domain: {0}  ", domainName);

            return new BatchPutAttributesRequest { DomainName = domainName };
        }
    }
}